命令式弹窗组件
在 Vue 中实现命令式弹窗组件(如 this.$dialog.show(args)),核心思路是通过 动态创建组件实例 并挂载到 DOM 上,结合配置项实现灵活的弹窗控制。以下是完整实现方案,支持默认确认框、自定义组件传入及丰富的配置项:
一、实现思路
- 基础结构:创建一个弹窗容器组件(
DialogContainer),负责渲染默认确认框或自定义组件。 - 命令式 API:封装
$dialog方法(如show、close),通过Vue.extend或createVNode动态创建组件实例。 - 配置系统:支持传入标题、内容、按钮文本、关闭逻辑等配置项。
- 组件通信:通过回调函数或 Promise 处理弹窗的确认/取消事件。
二、完整实现代码
1. 弹窗容器组件(DialogContainer.vue)
vue
<template>
<div class="dialog-mask" @click="handleMaskClose">
<div class="dialog-wrapper" :style="dialogStyle">
<!-- 关闭按钮 -->
<button v-if="config.showClose" class="dialog-close" @click="handleClose">
×
</button>
<!-- 标题 -->
<div v-if="config.title" class="dialog-title">{{ config.title }}</div>
<!-- 内容区域:默认文本/自定义组件 -->
<div class="dialog-content">
<!-- 自定义组件 -->
<component
v-if="config.component"
:is="config.component"
v-bind="config.props || {}"
v-on="config.events || {}"
/>
<!-- 默认文本内容 -->
<div v-else>{{ config.content }}</div>
</div>
<!-- 按钮区域 -->
<div class="dialog-footer" v-if="!config.hideFooter">
<button class="dialog-btn cancel" @click="handleCancel">
{{ config.cancelText || "取消" }}
</button>
<button class="dialog-btn confirm" @click="handleConfirm">
{{ config.confirmText || "确认" }}
</button>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, emit } from "vue";
// 定义配置项类型
const props = defineProps({
config: {
type: Object,
default: () => ({
title: "", // 标题
content: "", // 默认文本内容
component: null, // 自定义组件
props: null, // 自定义组件的props
events: null, // 自定义组件的事件
showClose: true, // 是否显示关闭按钮
hideFooter: false, // 是否隐藏底部按钮
cancelText: "取消", // 取消按钮文本
confirmText: "确认", // 确认按钮文本
closeOnMask: true, // 点击遮罩是否关闭
closeOnConfirm: true, // 点击确认后是否关闭
}),
},
});
const emit = defineEmits(["confirm", "cancel", "close"]);
// 点击遮罩关闭
const handleMaskClose = () => {
if (props.config.closeOnMask) {
emit("close");
}
};
// 点击关闭按钮
const handleClose = () => emit("close");
// 取消按钮
const handleCancel = () => {
emit("cancel");
emit("close"); // 取消后默认关闭
};
// 确认按钮
const handleConfirm = () => {
emit("confirm");
if (props.config.closeOnConfirm) {
emit("close");
}
};
</script>
<style scoped>
.dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.dialog-wrapper {
width: 500px;
background: #fff;
border-radius: 8px;
padding: 20px;
position: relative;
}
.dialog-close {
position: absolute;
top: 15px;
right: 15px;
background: transparent;
border: none;
font-size: 20px;
cursor: pointer;
}
.dialog-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
}
.dialog-content {
min-height: 100px;
margin-bottom: 20px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.dialog-btn {
padding: 8px 16px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
}
.dialog-btn.confirm {
background: #409eff;
color: white;
border-color: #409eff;
}
</style>2. 命令式 API 封装(dialog.js)
javascript
import { createVNode, render } from "vue";
import DialogContainer from "./DialogContainer.vue";
// 创建弹窗容器
const container = document.createElement("div");
document.body.appendChild(container);
// 存储当前弹窗实例(支持单例,如需多弹窗可改为数组)
let currentVNode = null;
export const dialog = {
// 显示弹窗
show(config = {}) {
return new Promise((resolve, reject) => {
// 关闭现有弹窗
if (currentVNode) {
this.close();
}
// 创建虚拟节点
currentVNode = createVNode(DialogContainer, {
config,
// 绑定事件
onConfirm: () => {
resolve("confirm"); // 确认时resolve
},
onCancel: () => {
reject("cancel"); // 取消时reject
},
onClose: () => {
this.close(); // 关闭弹窗
},
});
// 渲染到DOM
render(currentVNode, container);
});
},
// 关闭弹窗
close() {
if (currentVNode) {
render(null, container); // 卸载组件
currentVNode = null;
}
},
};
// 注册为Vue全局属性
export default {
install(app) {
app.config.globalProperties.$dialog = dialog;
},
};3. 注册到 Vue 应用
javascript
// main.js
import { createApp } from "vue";
import App from "./App.vue";
import dialogPlugin from "./dialog.js";
const app = createApp(App);
app.use(dialogPlugin); // 注册全局弹窗
app.mount("#app");三、使用方式
1. 基础确认框
javascript
// 在组件中调用
async function openDefaultDialog() {
try {
await this.$dialog.show({
title: "提示",
content: "确定要删除这条数据吗?",
confirmText: "删除",
cancelText: "再想想",
});
// 确认后的逻辑(如删除数据)
console.log("用户确认删除");
} catch (e) {
// 取消后的逻辑
console.log("用户取消操作");
}
}2. 传入自定义组件
假设存在自定义组件 CustomForm.vue:
vue
<template>
<div>
<input v-model="name" placeholder="请输入姓名" />
</div>
</template>
<script setup>
import { ref } from "vue";
const name = ref("");
// 暴露数据给父组件
defineExpose({ name });
</script>在弹窗中使用该组件:
javascript
import CustomForm from "./CustomForm.vue";
async function openCustomDialog() {
try {
// 存储自定义组件实例,用于获取内部数据
let formInstance = null;
await this.$dialog.show({
title: "自定义表单",
component: CustomForm,
// 监听组件事件(如有)
events: {
// 假设组件有change事件
change: (val) => console.log("表单变化:", val),
},
// 组件挂载后获取实例
onMounted: (instance) => {
formInstance = instance;
},
});
// 确认后获取组件数据
console.log("用户输入的姓名:", formInstance.name);
} catch (e) {
console.log("用户取消");
}
}3. 高级配置项
javascript
this.$dialog
.show({
title: "特殊配置",
content: "点击遮罩不关闭,无关闭按钮",
showClose: false, // 隐藏关闭按钮
closeOnMask: false, // 点击遮罩不关闭
hideFooter: false, // 显示底部按钮
closeOnConfirm: false, // 确认后不关闭(如需手动处理关闭)
})
.then(() => {
console.log("确认后不关闭,可手动处理");
// 处理完逻辑后手动关闭
this.$dialog.close();
});四、核心特性总结
- 默认确认框:支持标题、内容、按钮文本自定义,通过 Promise 处理确认/取消。
- 自定义组件:可传入任意 Vue 组件,支持传递 props 和事件,通过
defineExpose获取组件内部数据。 - 灵活配置:
showClose:是否显示关闭按钮closeOnMask:点击遮罩是否关闭hideFooter:是否隐藏底部按钮closeOnConfirm:确认后是否自动关闭- 按钮文本自定义等
五、扩展方向
- 多弹窗支持:将
currentVNode改为数组,支持同时显示多个弹窗。 - 动画效果:添加弹窗显示/隐藏的过渡动画(使用 Vue 的
<Transition>组件)。 - 样式定制:支持通过配置项传入自定义类名或样式。
- 加载状态:在确认按钮添加加载状态,防止重复提交。
通过这种实现,既能保持命令式调用的简洁性,又能兼顾 Vue 组件的灵活性,满足大部分弹窗场景需求。